<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./assets/global.css">

    <style>
        .mine-clearance {
            background-color: rgb(204, 204, 204);
            padding: 9px;
            display: inline-flex;
            flex-direction: column;
        }

        .tool-bar {
            height: 32px;
            border: 2px solid #808080;
            border-right-color: #fff;
            border-bottom-color: #fff;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        #grid {
            margin-top: 6px;
            border: 2px solid #808080;
            border-right-color: #fff;
            border-bottom-color: #fff;
        }

        #emote {
            border: 2px solid #808080;
            border-left-color: #fff;
            border-top-color: #fff;
            width: 21px;
            height: 21px;
        }

        #emote:active {
            border: 2px solid #808080;
            border-bottom: 1px solid #808080;
            border-right: 1px solid #808080;
        }
    </style>
</head>

<body>
    <div class="mine-clearance">
        <div class="tool-bar">
            <canvas id="count" width="39" height="23"></canvas>
            <img id="emote" />
            <canvas id="time" width="39" height="23"></canvas>
        </div>
        <canvas id="grid"></canvas>
    </div>



    <script type="module">
        import { Maths } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/maths.js";
        import { Randoms } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/randoms.js";

        /**
         * 加载图
         * @param {string} src
         * @returns {Promise<HTMLImageElement>}
         */
        function loadImage(src) {
            return new Promise((resolve) => {
                let image = new Image()
                image.src = src;
                image.onload = (ev) => {
                    resolve(image)
                }
            })
        }

        /**
         * 初始化平面数组
         * @author 	 linyisonger
         * @date 	 2025-01-13
         * 
         * @template T
         * @param {number} rows
         * @param {number} cols
         * @param {T|(x,y)=>T} init 
         * 
         * @return {T[][]}
         */
        function createFlatArray(rows, cols, init = 0) {
            let level1 = []
            for (let y = 0; y < cols; y++) {
                let level2 = [];
                for (let x = 0; x < rows; x++) {
                    level2[x] = typeof init == "function" ? init(x, y) : init;
                }
                level1[y] = level2
            }
            return level1
        }

        /**
         * 格子类型
         */
        const GRID_TYPE = {
            /** 空 */
            BLOCKED: 'blocked',
            /** 九宫格 炸弹数量 */
            NUM: 'num',
            /** 炸弹 */
            BOMB: 'bomb',
            /** 炸弹炸了 */
            BOMBED: 'bombed',
        }

        const GRID_STATE = {
            /** 空 */
            NONE: '',
            /** 块 */
            BLOCK: 'block',
            /** 旗帜 */
            FLAG: 'flag'
        }

        const EMOTE_TYPE = {
            /** 微笑 */
            SMILE: 'smile',
            /** 哭 */
            COOL: 'cool',
            /** 哭 */
            CRY: 'cry'
        }

        /**
         * 格子
         * @author 	 linyisonger
         * @date 	 2025-01-13
         */
        class Grid {
            state = GRID_STATE.BLOCK
            type = GRID_TYPE.BLOCKED

            x = 0;
            y = 0;
            count = 0;

            constructor(x, y) {
                this.x = x;
                this.y = y;
            }
        }

        const gameConfig = {
            rows: 9,
            cols: 9,
            bomb: 10,
        }

        const mainAssetConfig = {
            block: 'data:img/gif;base64,R0lGODlhGQAZAJEAAP///8DAwICAgAAAACH5BAQUAP8ALAAAAAAZABkAAAJKhI+pFrH/GpwnCFGb3nxfzHQi92XjWYbnmAIrepkvGauzV7s3Deq71vrhekJgrtgIIpVFptD5g+6kN+osZflot9xPsgvufsPkSwEAOw==',
            blocked: 'data:img/gif;base64,R0lGODlhGQAZAKIAAM7OzsbGxr6+vra2trKysqampoKCggAAACH5BAAHAP8ALAAAAAAZABkAAANHaLrc3mWQSau9xAwQuv9gGBhEIJxoqq4CabLw6sY0Otf0jcP6Lpc+HjD4exFTvWNrqDwlj09iNDj1VXdXXLa2zTGb3VgYlgAAOw==',
            1: 'data:img/gif;base64,R0lGODlhGQAZAJEAAMDAwICAgAAA/wAAACH5BAQUAP8ALAAAAAAZABkAAAJMjI+py70Ao5wUmorxzTxuLIRC9lFiSAbZOWqqybZVCcWoC8fpeu5gj/uJfBUWMXebvYpAJdJGoQFsTc8yQh1OpJ3otesEH8XbLzlSAAA7',
            2: 'data:img/gif;base64,R0lGODlhGQAZALMAAMDAwKu5q6i4qKW3pZy0nJaylpCwkIquioCAgACAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAHAP8ALAAAAAAZABkAAAReEMlJq704X8C7/yAnhSQ5lmh3cknrvi64wvT7zXV9I50N+ipeihXzrFACoEhYMtB2pSSsIGN6CrRBIHQEEGjb0pEmGOJch+Gyl3samW13kB0vztViKx665+r7a4AfEQA7',
            3: 'data:img/gif;base64,R0lGODlhGQAZAJEAAMDAwICAgP8AAAAAACH5BAQUAP8ALAAAAAAZABkAAAJOjI+py70Ao5wUmorxzTxuIITiOFIfiZbSmabT16le0EWyRdeg+OZTGzL5JECBsFI86m4AGIfpzEB9RVLPVuWtqFnjVZeJgmdjzbBMRk8KADs=',
            4: 'data:img/gif;base64,R0lGODlhGQAZAJEAAMDAwICAgAAAmQAAgCH5BAAHAP8ALAAAAAAZABkAAAJdjI+py70Ao5wUmorxzTzuOgjAQJLUN5FiaU5oVK7sGVAxxA50Gor47KrBVJLc7hfilY6jnHMp+TynwYjgisXmfB4hx1jtgKPezLjbaULJ6bOlnK684mG6HG5H5ykFADs=',
            5: 'data:img/gif;base64,R0lGODlhGQAZAJEAAMDAwICAgIAAAAAAACH5BAQUAP8ALAAAAAAZABkAAAJOjI+py70Ao5wUmorxzTxuIITiSE4fiY5mEKXu2oodDKmzx9bybeWgW6L9gDZcjLgz3oo9HhPw6TxPyGRTVxUIs1ohjxL9SsLia3nsO0MKADs=',
            6: 'data:img/gif;base64,R0lGODlhGQAZAJEAAMDAwICAgACAgAAAACH5BAQUAP8ALAAAAAAZABkAAAJTjI+py70Ao5wUmorxzTxuKITiKFIfiZbTmaJmEI3dCoPqbNWAjOdxS3r9gDyPjhiUsEKVos/GpDgBS0Hz9twhsdTjNmqUfIU9ja5MQ1c+6nBbUgAAOw==',
            7: 'data:img/gif;base64,R0lGODlhGQAZAJEAAL6+voKCggAAAAAAACH5BAAHAP8ALAAAAAAZABkAAAJMjI+py70Ao5wUmorxzTxuIITiSE4fiY5mEKXu2rUiHIOz9HElznY771NRcpQfrWI8TpJK2S1DtD2hPWeoRhTGojVgV1P9NsWWMBlSAAA7',
            8: 'data:img/gif;base64,R0lGODlhGQAZAIAAAMDAwICAgCH5BAQUAP8ALAAAAAAZABkAAAJKjI+py70Ao5wUmorxzTxu63xeMIYi8JnHlK4UwpLgOctjaUuwnqtvb/rxQsJaZXcz/k4tGsqVfPp4OCK1yih2OM7t1avJgWNjTAEAOw==',
            bomb: 'data:img/gif;base64,R0lGODlhGQAZAJEAAP///76+voKCggAAACH5BAAHAP8ALAAAAAAZABkAAAJglI+py70Bo5wUmhrHwPFyzVlCCIYeBGqqik5nkK4s7I6TjFOvCvR42bGhND3AbyPZEX2/GuwILV2iUZGEOgtesUhthuu8VcNbqE74fOZoSTQFiHm9u3G3OBSQ4yv6/aQAADs=',
            bombed: 'data:img/gif;base64,R0lGODlhGQAZAJEAAP///4CAgP8AAAAAACH5BAQUAP8ALAAAAAAZABkAAAJgjI+py70Co5wUmhrHwPFyzVlBCIYeBGqqik6nkK4s7I6TjFOvCvR42bGhND3AbyPZEX2/GuwILV2iUZGEOgtesUhthuu8VcNbqE74fOZoSTQFiHm9u3G3OCSQ4yv6/aQAADs=',
            flag: 'data:img/gif;base64,R0lGODlhGQAZAKIAAP///8DAwICAgP8AAAAAAAAAAAAAAAAAACH5BAAHAP8ALAAAAAAZABkAAANsCLrcriG8OSO9KwiRo//gt3FQaIJjZw7DmZYh25ovEMtzWH+47G6qno8GhAWEOVTRBur9SMxbUrQ8BQiEpyqE1RpBXSLUGtZVI9i0Wu3ZodfwbMR9ja/bZ6s3qjeTNCOBgoMjc4SHhIaIixsJADs=',
        }

        const toolAssetConfig = {
            0: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI5lI8Jy3wgmoMRymoc0vqt/G1hB1JkZVKKaQSC60YI/NZPW8dpApZrqvKhfkIPkIi5jFANWUkS5CUKADs=',
            1: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI3lI8psc33mDQAmilAdUnv1mkXeHhWuGGlOCpsxGXIpII0dYbjs358v0PlgDGiDfQKPj6cmkJRAAA7',
            2: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI8lI8Jy3whmgMvNmsgRC4nSwmaslRkOHKl962QVIYgfFAwta3JBu6i0UHwgp6cztQT/jCSZWTmGN5kn08BADs=',
            3: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI5lI8Jy3whmgMvNmsgRC4nSwmaslRkOHKl962QVIYgXMGUvJFebnRi6/qpcECU8OB7XYK2CNIm+3wKADs=',
            4: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI8lI8psc0HDJsmCmDptdhCXIFIF3rlOYpkyplftXWgJ9cHU8+3skkNu0usNB/PA+gQin4a0YPyMEF/PF4BADs=',
            5: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI5lI8Jy3wgmgtmxSZotYhql2gX1GUbWZ6heSWGCEkWLHNmHR0iwnmKvVOZgiAWr2cU3jBIHKpmc7kKADs=',
            6: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI5lI8Jy3wgmgtmxSZotYhql2gX1GUbWZ6heSWGCEkWLHPRiJE2woFPvxupeEDU0HeM5XQ4B6oWdCEKADs=',
            7: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI5lI8Jy3whmgMvNmsgRC4nSwmaslRkOHKl963buIUejLziHZnkfOpHl/qtcCoU0dDBpVy8WO0W/EgLADs=',
            8: 'data:img/gif;base64,R0lGODlhDQAXAIAAAP8AAAAAACH5BAQUAP8ALAAAAAANABcAAAI0jI8Jy3wQmoMRymoc0vqt/G1hB1JkZVKKiTaRJ4VvbIGJja6pWvbirvPBbJdRawKL3ZaGAgA7',
            9: 'data:img/gif;base64,R0lGODlhDQAXAJEAAP8AAIAAAAAAAAAAACH5BAQUAP8ALAAAAAANABcAAAI6lI8Jy3wgmoMRymoc0vqt/G1hB1JkZVKKiTaRJ4VC0E4HnWDzLqp4b6D9SrecRrjy1XS0mAUWy0kNBQA7',
            smile: 'data:img/gif;base64,R0lGODlhFQAVAJEAAAAAAP//AL29vQAAACH5BAAHAP8ALAAAAAAVABUAAAJAlI+py50AoUMwWCsduBy33XXAAoaiUlZY+nBq8MKUSY9HSbtzft4X/vu1MCLhcBXRoXgyBlD5AWYmgsiUis0yCgA7',
            cool: 'data:img/gif;base64,R0lGODlhFQAVAJEAAAAAAP//AMDAwICAACH5BAAHAP8ALAAAAAAVABUAAAJHlI+py50AoUMwWCsduBy33XXAAoaiAkZqFYwUprZx+27xTR/swMoD/DKZXIaSkEMsXoytJQo58zyPOdIxSWJmJoIV9wueFAAAOw==',
            cry: 'data:img/gif;base64,R0lGODlhFQAVAJEAAAAAAP//AMDAwAAAACH5BAAHAP8ALAAAAAAVABUAAAJDlI+py50AoUMwWCsduBy33XXAAm5gYHrPdWLs6p6parShSN36eNg6LbBFKhxejyV6wXwo4PGH+vyMDCKLqhlOttxuAQA7'
        }
        /**
         * 扩张
         * 
         * 0  0  0
         * 0  m  0  1
         * 0  0  0
         * 
         * 
         * @author 	 linyisonger
         * @date 	 2025-01-13
         * 
         * @template T
         * @param {{x:number,y:number}} v2
         * @param {number} depth
         * @param {T[][]} map
         * @returns {T[]}
         */
        function expansion(v2, depth, map) {
            let res = []
            for (let y = Math.max(v2.y - depth, 0); y < Math.min(v2.y + depth + 1, gameConfig.cols); y++) {
                for (let x = Math.max(v2.x - depth, 0); x < Math.min(v2.x + depth + 1, gameConfig.rows); x++) {
                    res.push(map[y][x])
                }
            }
            return res
        }



        // 地图
        let map = createFlatArray(gameConfig.rows, gameConfig.cols, (x, y) => new Grid(x, y))
        // 格子
        let grids = map.flat(2)
        /** @type {Grid[]} 炸弹格子*/
        let bombs = []
        /** @type {Grid[]} 数字格子*/
        let nums = []

        function initMap() {
            // 初始化
            grids.forEach(g => {
                g.count = 0;
                g.state = GRID_STATE.BLOCK
                g.type = GRID_TYPE.BLOCKED
            })
            // 随机炸弹
            bombs = Randoms.getDisorganizeArray(grids).slice(0, gameConfig.bomb)
            bombs.forEach(b => {
                // 设置为炸弹类型
                b.type = GRID_TYPE.BOMB
                // 筛选可能成为数字格子
                expansion(b, 1, map).filter(a => a.type != b.type).forEach(n => {
                    n.type = GRID_TYPE.NUM;
                    n.count++
                })
            })
        }

        /**
         * 空白格子探索
         * @author 	 linyisonger
         * @date 	 2025-01-14
         * 
         * @param {Grid} grid
         */
        function blockedExpansion(grid) {
            expansion(grid, 1, map).filter(g => g.state == GRID_STATE.BLOCK).forEach(g => {
                g.state = GRID_STATE.NONE;
                if (g != grid && g.type == GRID_TYPE.BLOCKED) blockedExpansion(g)
            })
        }

        /**
         * 游戏结束 踩雷了
         * @author 	 linyisonger
         * @date 	 2025-01-14
         * 
         * @param {Grid} grid
         */
        function bombed(grid) {
            grid.type = GRID_TYPE.BOMBED
            bombs.filter(b => b.state != GRID_STATE.FLAG).forEach(b => b.state = GRID_STATE.NONE)
            gridCanvas.removeEventListener("mousedown", mousedownFunc)
            stopTime();
            updEmote(EMOTE_TYPE.CRY)
        }


        /**
         * 检查游戏是否结束
         * @author 	 linyisonger
         * @date 	 2025-01-14
         */
        function checkOver() {
            let count = grids.filter(g => [GRID_STATE.BLOCK, GRID_STATE.FLAG].includes(g.state)).length
            // 游戏胜利 ✌
            if (count == gameConfig.bomb) {
                bombs.forEach(b => b.state = GRID_STATE.FLAG)
                stopTime();
                updEmote(EMOTE_TYPE.COOL)
                drawNums(countCtx, String(0).padStart(3, '0'))
            }
        }

        // 格子宽度
        let gs = 25;

        /** @type {HTMLCanvasElement} */
        let gridCanvas = document.querySelector("#grid")
        let gridCtx = gridCanvas.getContext('2d')
        gridCanvas.setAttribute('width', gameConfig.rows * gs)
        gridCanvas.setAttribute('height', gameConfig.cols * gs)
        let gridWidth = gridCanvas.clientWidth;
        let gridHeight = gridCanvas.clientHeight;
        gridCanvas.width = gridWidth;
        gridCanvas.height = gridHeight;

        /** @type {HTMLCanvasElement} */
        let countCanvas = document.querySelector("#count")
        let countCtx = countCanvas.getContext('2d')
        let countWidth = countCanvas.clientWidth;
        let countHeight = countCanvas.clientHeight;
        countCanvas.width = countWidth;
        countCanvas.height = countHeight;

        /** @type {HTMLCanvasElement} */
        let timeCanvas = document.querySelector("#time")
        let timeCtx = timeCanvas.getContext('2d')
        let timeWidth = timeCanvas.clientWidth;
        let timeHeight = timeCanvas.clientHeight;
        timeCanvas.width = countWidth;
        timeCanvas.height = countHeight;

        // 加载素材配置
        async function loadAssetConfig() {
            for (const key in mainAssetConfig) {
                mainAssetConfig[key] = await loadImage(mainAssetConfig[key])
            }
            for (const key in toolAssetConfig) {
                toolAssetConfig[key] = await loadImage(toolAssetConfig[key])
            }
        }

        // 渲染格子
        function drawGrids() {
            grids.forEach(g => {
                let x = g.x * gs;
                let y = g.y * gs;
                if (g.state) gridCtx.drawImage(mainAssetConfig[g.state], x, y)
                else if (g.type === GRID_TYPE.NUM) gridCtx.drawImage(mainAssetConfig[g.count], x, y)
                else gridCtx.drawImage(mainAssetConfig[g.type], x, y)
            })
        }

        /**
         * 渲染数字
         * @author 	 linyisonger
         * @date 	 2025-01-14
         * 
         * @param {CanvasRenderingContext2D} ctx
         * @param {string} num
         */
        function drawNums(ctx, num) {
            let charWidth = ctx.canvas.width / num.length
            for (let i = 0; i < num.length; i++) {
                const char = num[i];
                ctx.drawImage(toolAssetConfig[char], i * charWidth, 0)
            }
        }

        /**
         * 点击事件
         * 
         * @author 	 linyisonger
         * @date 	 2025-01-13
         * 
         * @param {{x:number,y:number}} v2 
         */
        function open(v2) {
            startTime();
            let { x, y } = v2;
            let grid = map[y][x]
            if (grid.state == GRID_STATE.FLAG) grid.state = GRID_STATE.BLOCK;
            else if (grid.type == GRID_TYPE.BLOCKED) blockedExpansion(grid)
            else if (grid.type == GRID_TYPE.BOMB) return bombed(grid)
            else grid.state = GRID_STATE.NONE;
            checkOver();
        }

        /**
         * 设置标志
         * 
         * @author 	 linyisonger
         * @date 	 2025-01-13
         * 
         * @param {{x:number,y:number}} v2 
         */
        function setFlag(v2) {
            let { x, y } = v2;
            let grid = map[y][x]

            if (grid.state == GRID_STATE.FLAG) grid.state = GRID_STATE.BLOCK;
            else if (grid.state == GRID_STATE.BLOCK && flag > 0) grid.state = GRID_STATE.FLAG;

            flag = gameConfig.bomb - grids.filter(g => g.state === GRID_STATE.FLAG).length;
            drawNums(countCtx, String(flag).padStart(3, '0'))
        }

        // 计时器
        let intervalId = null
        let time = 0;
        // 旗帜数量
        let flag = 0;
        // 表情类型
        let emoteDom = document.querySelector('#emote')

        /**
         * 开始计时
         * @author 	 linyisonger
         * @date 	 2025-01-14
         */
        function startTime() {
            if (intervalId) return;
            intervalId = setInterval(() => {
                time++;
                drawNums(timeCtx, String(time).padStart(3, '0'))
            }, 1000);
        }

        /**
         * 停止计时
         * @author 	 linyisonger
         * @date 	 2025-01-14
         */
        function stopTime() {
            clearInterval(intervalId)
            intervalId = null
        }

        /**
         * 更新表情
         * @author 	 linyisonger
         * @date 	 2025-01-14
         */
        function updEmote(type) {
            emoteDom.setAttribute("src", toolAssetConfig[type].src)
        }

        function mousedownFunc(e) {
            let x = ~~(e.offsetX / gs);
            let y = ~~(e.offsetY / gs)
            if (e.button == 0) open({ x, y })
            else if (e.button == 2) setFlag({ x, y })
            drawGrids()
        }

        function initTool() {
            time = 0;
            flag = gameConfig.bomb;
            drawNums(timeCtx, String(time).padStart(3, '0'))
            drawNums(countCtx, String(flag).padStart(3, '0'))
            updEmote(EMOTE_TYPE.SMILE)
        }

        function emoteClick() {
            initMap();
            initTool();
            drawGrids();
            gridCanvas.addEventListener("mousedown", mousedownFunc)
        }


        async function init() {
            await loadAssetConfig()
            emoteDom.addEventListener('click', emoteClick)
            emoteClick();
        }
        init();

        window.onload = function () {
            document.addEventListener('contextmenu', (e) => {
                if (e.button == 2) {
                    // 阻止事件传播，防止默认的右键菜单打开
                    e.preventDefault();
                }
            }, false);
        };
    </script>


</body>

</html>